package org.vaadinpa.logic;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.vaadinpa.Conf;
import org.vaadinpa.data.BattleNewsItem;
import org.vaadinpa.data.Coordinate;
import org.vaadinpa.data.Fleet;
import org.vaadinpa.data.Fleet.FleetOrder;
import org.vaadinpa.data.Planet;
import org.vaadinpa.data.Resource;
import org.vaadinpa.data.Ship;
import org.vaadinpa.data.Ship.ShipType;
import org.vaadinpa.data.Universe;

public class BattleCalculator {

	private static Map<Integer, Ship> initToShip = new HashMap<Integer, Ship>();
	private static Map<Fleet, Planet> fleetToPlanet = new HashMap<Fleet, Planet>();

	private static int battleID = 0;

	static {
		for (Ship s : Ship.getAvailableShips()) {
			initToShip.put(s.getInit(), s);
		}
	}

	public static synchronized void checkForAndPerformBattles() {
		for (Planet planet : Universe.instance.values()) {
			for (Fleet f : planet.fleets) {
				fleetToPlanet.put(f, planet);
			}
		}

		for (Planet planet : Universe.instance.values()) {

			BattleNewsItem newsItem = new BattleNewsItem(planet, battleID++);

			Coordinate coords = planet.coords;

			// find all fleets in the area
			Set<Fleet> attackers = new HashSet<Fleet>();
			Set<Fleet> defenders = new HashSet<Fleet>();

			defenders.add(planet.homeFleet);
			newsItem.addShips(planet, planet.homeFleet, true);
			for (Planet p : Universe.instance.values()) {
				for (Fleet f : p.fleets) {
					if (f.atPlanet(coords)) {
						if (f.currentOrder == FleetOrder.ATTACK) {
							attackers.add(f);
						} else if (f.currentOrder == FleetOrder.DEFEND) {
							defenders.add(f);
						}
						newsItem.addShips(p, f, true);
					}
				}
			}
			newsItem.fillRoids(true);

			if (attackers.size() == 0) {
				continue;
			}

			List<Integer> inits = new ArrayList<Integer>(initToShip.keySet());
			Collections.sort(inits);
			for (int init : inits) {

				Ship shipNowFiring = initToShip.get(init);

				// calculate damage

				// attackers shoot at defenders
				int attackingDamage = collectDamage(attackers, init);
				// defenders shoot at attackers
				int defendingDamage = collectDamage(defenders, init);

				if (shipNowFiring.getPrimaryTarget() == ShipType.ASTEROID) {

					Map<Resource, Integer> availableRoids = planet.giveUpRoids(attackingDamage);
					deliverRoids(availableRoids, attackers, newsItem);

				} else {

					// deliver damage

					// primary target hits
					attackingDamage = deliverDamage(defenders, shipNowFiring.getPrimaryTarget(), attackingDamage);
					// adjust the damage
					attackingDamage *= Conf.SECONDARY_TARGET_MULTIPLIER;
					// secondary hits
					deliverDamage(defenders, shipNowFiring.getSecondaryTarget(), attackingDamage);

					// primary target hits
					defendingDamage = deliverDamage(attackers, shipNowFiring.getPrimaryTarget(), defendingDamage);
					// adjust the damage
					defendingDamage *= Conf.SECONDARY_TARGET_MULTIPLIER;
					// secondary hits
					deliverDamage(attackers, shipNowFiring.getSecondaryTarget(), defendingDamage);

				}
			}

			System.out.println("Battle on " + planet.name + " resolved");

			// fill aftermath to report
			Set<Planet> affectedPlanets = new HashSet<Planet>();
			for (Planet p : Universe.instance.values()) {
				for (Fleet f : p.fleets) {
					if (f.atPlanet(coords)) {
						newsItem.addShips(p, f, false);
						affectedPlanets.add(p);
					}
				}
			}
			newsItem.addShips(planet, planet.homeFleet, false);
			newsItem.fillRoids(false);

			for (Planet p : affectedPlanets) {
				p.addNews(newsItem);
			}
		}

		fleetToPlanet.clear();
	}

	private static void deliverRoids(Map<Resource, Integer> availableRoids, Set<Fleet> attackers, BattleNewsItem news) {
		Map<Fleet, Integer> powerInFleets = new HashMap<Fleet, Integer>();

		// how much roid firepower there is in all fleets combined
		int totalpower = 0;
		for (Fleet f : attackers) {
			int amountFirePower = 0;
			for (Ship ship : Ship.getAvailableShips()) {
				if (ship.getPrimaryTarget() == ShipType.ASTEROID) {

					int powerForThis = f.get(ship) * ship.getPower();

					amountFirePower += powerForThis;
					totalpower += powerForThis;
				}
			}
			powerInFleets.put(f, amountFirePower);
		}

		for (Fleet f : powerInFleets.keySet()) {

			// distribute
			float sliceForThis = totalpower / (float) powerInFleets.get(f);

			// deliver
			for (Resource r : availableRoids.keySet()) {
				// takes int part from double
				int roidsForThis = Math.round(availableRoids.get(r) * sliceForThis);
				fleetToPlanet.get(f).addRoids(r, roidsForThis);
				news.addTakenRoids(r, roidsForThis, fleetToPlanet.get(f));
			}
		}

		// could calculate roids that are left because of fraction errors.. but
		// what's the point? lets just say they were destroyed during battle

	}

	/**
	 * Gathers the collective damage of all ships of init <code>init</code> in
	 * the <code>fleets</code>
	 */
	private static int collectDamage(Collection<Fleet> fleets, int init) {
		int damage = 0;
		for (Fleet fleet : fleets) {
			damage += fleet.fire(init);
		}
		return damage;
	}

	/**
	 * Distributes and delivers the damage according to fleet compositions
	 */
	private static int deliverDamage(Collection<Fleet> fleets, ShipType target, int damage) {

		Map<Fleet, Integer> shipsInFleets = new HashMap<Fleet, Integer>();

		// how many ships of type 'target' there are in all fleets combined
		int totalShips = 0;
		for (Fleet f : fleets) {
			int numOfTypeShips = 0;
			for (Ship ship : Ship.getAvailableShips()) {
				if (ship.getType() == target) {
					numOfTypeShips += f.get(ship);
					totalShips += f.get(ship);
				}
			}
			shipsInFleets.put(f, numOfTypeShips);
		}

		// how much damage is left over if all ships in fleet are destroyed
		for (Fleet f : shipsInFleets.keySet()) {

			double ratioOfShipsInThisFleet;
			if (shipsInFleets.get(f) != 0) {
				ratioOfShipsInThisFleet = totalShips / (double) shipsInFleets.get(f);
			} else {
				ratioOfShipsInThisFleet = 0;
			}

			// distribute
			int damageToThisFleet = (int) (ratioOfShipsInThisFleet * damage);

			// deliver
			int damageLeft = f.takeHit(target, damageToThisFleet);

			damage = damage - (damageToThisFleet - damageLeft);
		}

		return damage;
	}
}
